// $Id$
// Licensed under Apache License Version 2.0
// See: http://www.apache.org/licenses/LICENSE-2.0.html
package de.servicezombie.tcmdb.dao.mongo;

import java.lang.reflect.Field;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.util.Assert;

import de.servicezombie.lang.ExceptionUtils;
import de.servicezombie.tcmdb.TcmdbRecordNotFoundException;

public abstract class AbstractSimpleCrudDaoImpl<DomainObject extends MongoDomainObject> implements SimpleCrudDao<DomainObject> {

	private static final Logger log = LoggerFactory.getLogger(AbstractSimpleCrudDaoImpl.class);
	
	private MongoTemplate mongoTemplate;

	public void setMongoTemplate(MongoTemplate mongoTemplate) {
		this.mongoTemplate = mongoTemplate;
	}

	protected abstract Class<? extends MongoDomainObject> getDomainClass();

	private TcmdbRecordNotFoundException buildRecordNotFoundException(Object identifier) {
		String idAsString;
		
		if(identifier instanceof Criteria) {
			idAsString = MongoUtils.toString((Criteria) identifier);
		}
		else {
			idAsString = identifier != null ? identifier.toString() : null;
		}
		
		return new TcmdbRecordNotFoundException("Record not available: " + idAsString + " for type " + getDomainClass().getName());
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public DomainObject get(String id) {
		if(id == null) {
			return null;
		}
		@SuppressWarnings("unchecked")
		DomainObject result = (DomainObject) mongoTemplate.findById(id, getDomainClass());
		return result;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public DomainObject load(String id) throws TcmdbRecordNotFoundException {
		DomainObject result = get(id);
		if (result == null) {
			throw buildRecordNotFoundException(id);
		}
		return result;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public DomainObject loadUnique(Criteria criteria) throws TcmdbRecordNotFoundException {
		List<DomainObject> result = findByCriteria(criteria);
		if (log.isDebugEnabled()) {
			log.debug("loadUnique: lookup of " + MongoUtils.toString(criteria) + " returned " + result);
		}
		
		if (result.size() != 1) {
			throw buildRecordNotFoundException(criteria);
		}
		return result.get(0);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public DomainObject saveReplace(DomainObject object) {
		
		// find unique record
		String id = object.getId();
		load(id);
		
		mongoTemplate.save(object);
		return object;
		
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public DomainObject saveNewOrReplace(DomainObject object) {
		mongoTemplate.save(object);
		return object;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public DomainObject saveNew(DomainObject object) {		
		mongoTemplate.insert(object);		
		return object;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void delete(String id) {		
		Query query = null;
		mongoTemplate.remove(query , getDomainClass());
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	@SuppressWarnings("unchecked")
	public List<DomainObject> findAll() {
		return (List<DomainObject>) mongoTemplate.findAll(getDomainClass());
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public List<DomainObject> findByCriteria(Criteria criteria) {
		Query query = new Query(criteria);
		@SuppressWarnings("unchecked")
		List<DomainObject> result = (List<DomainObject>) mongoTemplate.find(query , getDomainClass());
		return result;
	}

	@Override
	public <V> Collection<V> findGroupedValues(final String fieldName) {
		
		List<DomainObject> allItems = findAll();
		Set<V> result = new HashSet<V>();

		for(Object o : allItems) {
			addValidFieldValue(fieldName, o, result);
		}
		
		return result;
	}

	private <V> void addValidFieldValue(String fieldName, Object bean, Set<V> resultSet) {
		
		if (log.isDebugEnabled()) {
			log.debug("getFieldValueOrNull: >> " + fieldName + ", " + bean);
		}
		
		Assert.notNull(bean, "bean may not be null");
		Assert.notNull(fieldName, "fieldName may not be null");
		
		try {
			Field field = bean.getClass().getDeclaredField(fieldName);
			field.setAccessible(true);
			
			@SuppressWarnings("unchecked")
			V value = (V) field.get(bean);
			resultSet.add(value);
		}
		catch (SecurityException e) {
			ExceptionUtils.logWarningWithStacktrace(log, e.getMessage(), e);
		}
		catch (IllegalArgumentException e) {
			ExceptionUtils.logWarningWithStacktrace(log, e.getMessage(), e);
		}
		catch (NoSuchFieldException e) {
			ExceptionUtils.logWarningWithStacktrace(log, e.getMessage(), e);
		}
		catch (IllegalAccessException e) {
			ExceptionUtils.logWarningWithStacktrace(log, e.getMessage(), e);
		}
		catch (ClassCastException e) {
			ExceptionUtils.logWarningWithStacktrace(log, "field exist with type, but has not expected type", e);
		}
		
		if (log.isDebugEnabled()) {
			log.debug("getFieldValueOrNull: << ");
		}
	}
	
	
}
